﻿using System;
using System.Numerics;
using System.Text;

using Unity.Type;
using Adaptive.Agrona;
using Adaptive.Agrona.Concurrent;

using Microsoft.Extensions.Logging;
using Unity.Type.BinaryType;

using Unity.Commons.Client.Threading;
using Unity.Logger;

namespace Unity.Commons.Client.Generator
{
    public sealed class UnityGenerator
    {
        private static readonly ILogger logger = UnityLogger.ConsoleLoggerFactory.CreateLogger(nameof(UnityGenerator));

        public const int MessageLength =
                MessageHeader.Size + MetadataE.BlockLength;

        public const int MaximumIdLength = 
            MetadataE.CsiIdLength + MetadataE.DelimiterLength +
            MetadataE.SpecVersionLength + MetadataE.RandomLength +
            MetadataE.CounterLength;

        public const ByteOrder DefaultByteOrder = ByteOrder.LittleEndian;

        public const byte Delimeter = (byte)':';

        public const int SpecVersion = 1;
        public const int FormatVersion = 1;

        public const int CounterLength = 5;
        public const int RandomLength = 19;
        public const int SystemIdLength = 8;

        public enum CounterStatus
        {
            Normal = 0,
            Overflow = -1
        }

        private readonly AtomicInteger counter = new AtomicInteger(UnityId.CounterMinValue - 1);

        private readonly IMutableDirectBuffer templateBuffer = new UnsafeBuffer(new byte[MessageLength]);

        private readonly char[] symbols = "01234567890ABCDEFGHIJKLMNOPRQRSTUVWXYZ".ToCharArray();

        private readonly int leadingZeroes;

        private static byte[] IntToBase36(int value)
        {
            char[] characters = "01234567890ABCDEFGHIJKLMNOPRQRSTUVWXYZ".ToCharArray();
            string buffer = string.Empty;
            int targetBase = characters.Length;

            do
            {
                buffer = characters[value % targetBase] + buffer;
                value /= targetBase;
            } while (value > 0);

            return Encoding.ASCII.GetBytes(buffer);
        }

        public int GetNonZeroIdLength()
        {
            return MaximumIdLength - leadingZeroes;
        }

        public UnityGenerator(string csiId)
        {
            MessageHeader headerEncoder = new MessageHeader();
            MetadataE metadataEncoder = new MetadataE();

            metadataEncoder.WrapForEncodeAndApplyHeader(templateBuffer, 0, headerEncoder);

            byte[] random = new byte[11];
            //new Xoroshiro128Plus();
            Random rnd = new Random();
            rnd.NextBytes(random);
             
            // byte[] base36random = IntToBase36(random);
            byte[] randomPart = Encoding.ASCII.GetBytes(ByteArrayToBase36String(random));

            int randomZeroes = RandomLength - randomPart.Length;
            for (int i = 0; i < RandomLength; i++)
            {
                if (i < randomZeroes)
                {
                    metadataEncoder.SetRandom(i, (byte)'0');
                }
                else
                {
                    metadataEncoder.SetRandom(i, randomPart[i - randomZeroes]);
                }
            }
            byte[] systemIdBytes = Encoding.ASCII.GetBytes(csiId);
            int systemIdZeroes = SystemIdLength - systemIdBytes.Length;
            leadingZeroes = systemIdZeroes;

            for (int i = 0; i < SystemIdLength; i++)
            {
                if (i < systemIdZeroes)
                {
                    metadataEncoder.SetCsiId(i, (byte)'0');
                }
                else
                {
                    metadataEncoder.SetCsiId(i, systemIdBytes[i - systemIdZeroes]);
                }
            }

            metadataEncoder.Delimiter = Delimeter;
            metadataEncoder.SpecVersion = ((byte)SpecVersion);
            metadataEncoder.ChangerCsiId = Int32.Parse(csiId);
            metadataEncoder.FormatVer = FormatVersion;
            metadataEncoder.VersionDelete = 0;

        }

        private string ByteArrayToBase36String(byte[] data)
        {
            BigInteger dividend = new BigInteger(data);
            var buffer = new StringBuilder();
            while (dividend != 0)
            {
                dividend = BigInteger.DivRem(dividend, 36, out BigInteger remainder);
                buffer.Insert(0, symbols[Math.Abs((int)remainder)]);
            }
            return buffer.ToString();
        }

        public int GetCounter()
        {
            return counter.Value + 1;
        }

        public void SetCounter(int value)
        {
            counter.Value = value - 1;
        }

        private int IncrementCounter()
        {
            int nextCounter;
            do
            {
                nextCounter = counter.Value + 1;
                if (nextCounter > UnityId.CounterMaxValue)
                {
                    return (int)CounterStatus.Overflow;
                }
            } while (!counter.CompareAndSet(nextCounter - 1, nextCounter));
            return nextCounter;
        }

        private void SetCounterValueInBuffer(IMutableDirectBuffer buffer, int offset, int counter)
        {
            int position = CounterLength - 1;
            while (counter >= UnityId.Radix)
            {
                buffer.PutByte(offset + position--, (byte)symbols[counter % UnityId.Radix]);
                counter /= UnityId.Radix;
            }
            buffer.PutByte(offset + position, (byte)symbols[counter]);

            while (position > 0)
            {
                buffer.PutByte(offset + --position, (byte)'0');
            }
        }

        public int GenerateMetadata(EntityType entityType, long version,
                IMutableDirectBuffer buffer, int offset)
        {
            buffer.PutBytes(offset, templateBuffer, 0, MessageLength);

            int counterValue = IncrementCounter();
            if (counterValue == (int)CounterStatus.Overflow)
            {
                return (int)CounterStatus.Overflow;
            }
            SetCounterValueInBuffer(buffer, offset + MessageHeader.Size + MetadataE.CounterOffset, counterValue);
            long unixTimestamp = UnityTimestamp.ClockTimestamp();
            buffer.PutLong(MetadataE.TimestampOffset + offset + MessageHeader.Size, unixTimestamp, DefaultByteOrder);
            buffer.PutLong(MetadataE.VersionOffset + offset + MessageHeader.Size, version, DefaultByteOrder);
            buffer.PutByte(MetadataE.EntityTypeOffset + offset + MessageHeader.Size, (byte)entityType);
            buffer.PutByte(MetadataE.EventTypeOffset + offset + MessageHeader.Size, (byte)EventType.Create);
            return (int)CounterStatus.Normal;
        }

        public int GenerateMetadataAndCopy(EntityType entityType, long version,
            IMutableDirectBuffer sourceBuffer, int sourceOffset,
            IMutableDirectBuffer destinationBuffer, int destinationOffset)
        {
            if (GenerateMetadata(entityType, version, sourceBuffer, sourceOffset) == (int)CounterStatus.Overflow)
            {
                return (int)CounterStatus.Overflow;
            }
            return GetUnityIdFromMetadata(sourceBuffer, sourceOffset, destinationBuffer, destinationOffset);
        }

        public void UpdateMetadata(IMutableDirectBuffer tempBuffer, int tempOffset,
            int idLength, EntityType entityType, long version,
            IMutableDirectBuffer buffer, int offset)
        {
            buffer.PutBytes(offset, templateBuffer, 0, MessageLength);
            int zeros = MaximumIdLength - idLength;
            if(idLength < MaximumIdLength)
            {
                int zeroCounter = 0;
                while(zeroCounter < zeros)
                {
                    buffer.PutByte(offset + MessageHeader.Size + zeroCounter, (byte)'0');
                    zeroCounter++;
                }
            }
            buffer.PutBytes(offset + MessageHeader.Size + zeros, tempBuffer, tempOffset, idLength);
            buffer.PutLong(MetadataE.TimestampOffset + offset + MessageHeader.Size, UnityTimestamp.ClockTimestamp());
            buffer.PutLong(MetadataE.VersionOffset + offset + MessageHeader.Size, version);
            buffer.PutByte(MetadataE.EntityTypeOffset + offset + MessageHeader.Size, (byte)entityType);
            buffer.PutByte(MetadataE.EventTypeOffset + offset + MessageHeader.Size, (byte)EventType.Update);
        }

        public int GenerateId(IMutableDirectBuffer buffer, int offset)
        {
            int unityIdLength = GetNonZeroIdLength();
            buffer.PutBytes(offset, templateBuffer, MessageHeader.Size + leadingZeroes, unityIdLength);
            int counterValue = IncrementCounter();
            if (counterValue == (int)CounterStatus.Overflow)
            {
                return (int)CounterStatus.Overflow;
            }
            SetCounterValueInBuffer(buffer, offset - leadingZeroes + MetadataE.CounterOffset, counterValue);
            return unityIdLength;
        }

        public int GetUnityIdFromMetadata(IMutableDirectBuffer sourceBuffer, int sourceOffset, IMutableDirectBuffer destinationBuffer, int destinationOffset)
        {
            int unityIdLength = GetNonZeroIdLength();
            destinationBuffer.PutBytes(destinationOffset, sourceBuffer, sourceOffset + MessageHeader.Size + leadingZeroes, unityIdLength);
            return unityIdLength;
        }

        public static long GetVersionFromMetadata(IMutableDirectBuffer buffer, int offset)
        {
            return buffer.GetLong(MetadataE.VersionOffset + offset + MessageHeader.Size, DefaultByteOrder);
        }

        public static void UpdateTime(IMutableDirectBuffer buffer, int offset, long version)
        {
            buffer.PutLong(MetadataE.VersionOffset + offset + MessageHeader.Size, version, DefaultByteOrder);
            buffer.PutLong(MetadataE.TimestampOffset + offset + MessageHeader.Size, UnityTimestamp.ClockTimestamp(), DefaultByteOrder);
        }

        public static string GetUnityId(IMutableDirectBuffer buffer, int offset, int length)
        {
            byte[] data = new byte[length];
            buffer.GetBytes(offset, data, 0, length);
            return Encoding.ASCII.GetString(data, 0, data.Length);
        }

        public static long GetVersion(IMutableDirectBuffer buffer, int offset, int length)
        {
            return buffer.GetLong(offset + length, DefaultByteOrder);
        }

    }

}